package core;

import com.sun.jdi.DoubleValue;
import core.data.DataHandle;
import core.validate.*;
import core.validate.rules.*;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class Validate<T> {
    private final List<Rule<?>> rules;
    private T entity;
    public Validate(List<Rule<?>> rules, T entity){
        this.rules = rules;
        this.entity = entity;
    }

    public Validate(List<Rule<?>> rules){
        this.rules = rules;
    }

    private void setEntityValue(Class<?> type, Object entity, String key, Object value){
        if(entity == null) return ;
        try{
            Field field = type.getDeclaredField(key);
            field.setAccessible(true);
            field.set(entity, value);
        } catch (NoSuchFieldException | IllegalAccessException ignored) {}
    }

    public boolean check(DataHandle<Map<String, Object>, T> data){
        return this.check(data, entity);
    }

    public boolean check(DataHandle<Map<String, Object>, T> data, Class<T> _entity){
        try {
            Constructor<T> constructor = _entity.getDeclaredConstructor();
            return check(data, constructor.newInstance());
        } catch (InvocationTargetException | NoSuchMethodException | InstantiationException | IllegalAccessException e) {
            // throw new RuntimeException(e);
            data.failure(e.getMessage());
            return false;
        }
    }
    public boolean check(DataHandle<Map<String, Object>, T> data, T _entity){
        if(_entity != null){
            entity = _entity;
        }

        Class<?> type = entity == null ? null : entity.getClass();
        getRuleByEntity();
        Map<String, Object> request = data.getParams();
        List<String> keys = new ArrayList<>(request.keySet());
        Map<String, String> messages = new HashMap<>();

        Holder holder = new Holder(request);

        for(Rule<?> rule: rules){
            String key = rule.key;
            if(!keys.contains(key)){
                for(String alias: rule.getAlias()){
                    if(keys.contains(alias)){
                        key = alias;
                        break;
                    }
                }
            }
            holder.setHandle(key);
            if(rule.hasPreCheck()){
                Object preValue = rule.doPreCheck(holder.getValue());
                holder.setValue(preValue);
                holder.setOrigin(holder.getHandle(), preValue);
            }
            if(holder.isEmpty() && !rule.isRequire()){
                setEntityValue(type, entity, rule.key, null);
                continue;
            }

            for(RuleAction action: rule.actions){
                action.apply(holder);
                if(!holder.getStatus()){
                    messages.put(rule.key, holder.getMessage());
                    break;
                }
            }

            if(holder.getStatus()){
                setEntityValue(type, entity, rule.key, holder.getValue());
            }
        }

        boolean checked = holder.getGlobalStatus();
        if(checked){
            data.success(entity);
        }else{
            data.failure(messages);
        }
        return checked;
    }

    private void getRuleByEntity() {
        Field[] fields = entity.getClass().getDeclaredFields();
        for (Field field : fields) {
            field.setAccessible(true);
            Rules rules = field.getAnnotation(Rules.class);
            if (rules == null) continue;
            RuleArray[] Arrays = rules.Arrays();
            RuleBoolean[] Booleans = rules.Booleans();
            RuleDate[] Dates = rules.Dates();
            RuleFile[] Files = rules.Files();
            RuleNumber[] Numbers = rules.Numbers();
            RuleString[] Strings = rules.Strings();
            Rule<?> rule = null;
            if (Arrays.length > 0) {
                rule = parseArrayRule(field.getName(), Arrays);
            } else if (Booleans.length > 0) {
                rule = parseBooleanRule(field.getName(), Booleans);
            } else if (Dates.length > 0) {
                rule = parseDateRule(field.getName(), Dates);
            } else if (Files.length > 0) {
                rule = parseFileRule(field.getName(), Files);
            } else if (Numbers.length > 0) {
                rule = parseNumberRule(field.getName(), Numbers);
            } else if (Strings.length > 0) {
                rule = parseStringRule(field.getName(), Strings);
            }
            if (rule == null) continue;
            String message = rules.message();
            if(!message.isEmpty()){
                rule.setMessage(message);
            }
            String[] alias = rules.alias();
            if(alias.length > 0){
                rule.setAlias(alias);
            }

            String preCheck = rules.preCheck();
            if(!preCheck.isEmpty()){
                try {
                    Method method = entity.getClass().getDeclaredMethod(preCheck, Object.class);
                    method.setAccessible(true);
                    RulePreCheck func = (Object value) -> {
                        try {
                            return method.invoke(entity, value);
                        }catch (InvocationTargetException | IllegalAccessException e){
                            throw new RuntimeException(e);
                        }
                    };
                    rule.onPreCheck(func);
                }catch (NoSuchMethodException e){
                    throw new RuntimeException(e);
                }
            }
            // 加入到组中
            this.rules.add(rule);
        }
    }

    private Rule<?> parseArrayRule(String key, RuleArray[] arrays){
        ArrayRule rule = new ArrayRule(key);
            for(RuleArray _array: arrays) {
                ArrayType arrayType = _array.type();
                if (arrayType == ArrayType.ITEM) {
                    RuleArray.Items item = _array.item();
                    if(item == null) continue;
                    RuleBoolean[] Booleans = item.Booleans();
                    RuleDate[] Dates = item.Dates();
                    RuleFile[] Files = item.Files();
                    RuleNumber[] Numbers = item.Numbers();
                    RuleString[] Strings = item.Strings();
                    Rule<?> itemRule = null;
                    if (Booleans.length > 0) {
                        itemRule = parseBooleanRule("item", Booleans);
                    } else if (Dates.length > 0) {
                        itemRule = parseDateRule("item", Dates);
                    } else if (Files.length > 0) {
                        itemRule = parseFileRule("item", Files);
                    } else if (Numbers.length > 0) {
                        itemRule = parseNumberRule("item", Numbers);
                    } else if (Strings.length > 0) {
                        itemRule = parseStringRule("item", Strings);
                    }
                    List<Object> objs = new ArrayList<>();
                    if(itemRule != null) objs.add(itemRule);
                    arrayType.apply(rule, objs, null);
                } else {
                    arrayType.apply(rule, parseParams(_array.params()), _array.message());
                }
            }
        return rule;
    }

    private Rule<?> parseBooleanRule(String key, RuleBoolean[] booleans){
        BooleanRule rule = new BooleanRule(key);
        for(RuleBoolean _boolean: booleans){
            BooleanType booleanType = _boolean.type();
            booleanType.apply(rule, parseParams(_boolean.params()), _boolean.message());
        }
        return rule;
    }

    private Rule<?> parseDateRule(String key, RuleDate[] dates){
        DateRule rule = new DateRule(key);
        for(RuleDate _date: dates){
            DateType dateType = _date.type();
            dateType.apply(rule, parseParams(_date.params()), _date.message());
        }
        return rule;
    }

    private Rule<?> parseFileRule(String key, RuleFile[] files){
        FileRule rule = new FileRule(key);
        for(RuleFile _file: files){
            FileType fileType = _file.type();
            fileType.apply(rule, parseParams(_file.params()), _file.message());
        }
        return rule;
    }

    private Rule<?> parseNumberRule(String key, RuleNumber[] numbers){
        NumberRule rule = new NumberRule(key);
        for(RuleNumber _number: numbers){
            NumberType numberType = _number.type();
            numberType.apply(rule, parseParams(_number.params()), _number.message());
        }
        return rule;
    }

    private Rule<?> parseStringRule(String key, RuleString[] strings){
        StringRule rule = new StringRule(key);
        for(RuleString _string: strings){
            StringType stringType = _string.type();
            stringType.apply(rule, parseParams(_string.params()), _string.message());
        }
        return rule;
    }

    private List<Object> parseParams(Param[] params){
        List<Object> list = new ArrayList<>();

        for(Param param: params){
            Double number = param.number();
            Object string = param.string();
            Object function = param.function();
            Object date = param.date();

            if(!Objects.equals(number, Double.NaN)){
                String numStr = String.valueOf(number);
                if(Pattern.compile("^-?\\d+\\.0+$").matcher(numStr).matches()){
                    list.add(number.intValue());
                }else if(Pattern.compile("^-?\\d+\\.\\d+$").matcher(numStr).matches()){
                    list.add(number);
                }
            }else if(!Objects.equals(string, "\0")){
                list.add(string);
            }else if(!Objects.equals(function, "\0")){
                String funcName = String.valueOf(function);
                try{
                    Method method = entity.getClass().getDeclaredMethod(funcName, Holder.class);
                    method.setAccessible(true);
                    RuleTest func = holder -> {
                        try {
                            return (boolean) method.invoke(entity, holder);
                        }catch (IllegalAccessException | InvocationTargetException e){
                            throw new RuntimeException(e);
                        }
                    };
                    list.add(func);
                }catch (NoSuchMethodException e){
                    throw new RuntimeException(e);
                }
            }else if(!Objects.equals(date, "\0")){
                String dateStr = String.valueOf(date);
                DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
                    .withZone(ZoneId.of("Asia/Shanghai"));
                LocalDateTime ldt = LocalDateTime.parse(dateStr, formatter);
                list.add(Date.from(ldt.atZone(ZoneId.systemDefault()).toInstant()));
            }
        }
        return list;
    }
}
